package lt.marsrutai.mobile.android.service.impl;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lt.marsrutai.mobile.android.dao.DataDao;
import lt.marsrutai.mobile.android.dao.dto.ArrivalTimeDto;
import lt.marsrutai.mobile.android.dao.dto.BusStopDto;
import lt.marsrutai.mobile.android.dao.dto.BusTrackDto;
import lt.marsrutai.mobile.android.dao.dto.DaysOfWeekDto;
import lt.marsrutai.mobile.android.dao.dto.GeoPointDto;
import lt.marsrutai.mobile.android.dao.dto.RouteDto;
import lt.marsrutai.mobile.android.dao.dto.ScheduleDto;
import lt.marsrutai.mobile.android.dao.dto.ScopeInfoDto;
import lt.marsrutai.mobile.android.dao.dto.SearchResultDto;
import lt.marsrutai.mobile.android.dao.dto.SegmentDto;
import lt.marsrutai.mobile.android.dao.dto.StopDayTimesDto;
import lt.marsrutai.mobile.android.dao.dto.TransportDto;
import lt.marsrutai.mobile.android.dao.exception.BackendException;
import lt.marsrutai.mobile.android.dao.exception.NoConnectivityException;
import lt.marsrutai.mobile.android.dao.exception.NoDataException;
import lt.marsrutai.mobile.android.exception.DataAccessException;
import lt.marsrutai.mobile.android.exception.UnableAccessDataException;
import lt.marsrutai.mobile.android.model.ArrivalTime;
import lt.marsrutai.mobile.android.model.DayGroup;
import lt.marsrutai.mobile.android.model.MobileScope;
import lt.marsrutai.mobile.android.model.Route;
import lt.marsrutai.mobile.android.model.RouteSearchResult;
import lt.marsrutai.mobile.android.model.RouteSearchStatus;
import lt.marsrutai.mobile.android.model.Schedule;
import lt.marsrutai.mobile.android.model.Segment;
import lt.marsrutai.mobile.android.model.SegmentPoint;
import lt.marsrutai.mobile.android.model.Stop;
import lt.marsrutai.mobile.android.model.StopArrivalTimes;
import lt.marsrutai.mobile.android.model.Track;
import lt.marsrutai.mobile.android.model.Transport;
import lt.marsrutai.mobile.android.model.enums.DateTime;
import lt.marsrutai.mobile.android.model.enums.TrackType;
import lt.marsrutai.mobile.android.model.enums.TransportType;
import lt.marsrutai.mobile.android.model.enums.WeekdayType;
import lt.marsrutai.mobile.android.service.DataService;
import lt.marsrutai.mobile.android.util.StringUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Data service.
 *
 * @author Martynas Mickevicius
 *
 */
public class DataServiceImpl implements DataService {

    private static final Logger logger = LoggerFactory.getLogger("DataServiceImpl");

    private DataDao dao;

    public DataServiceImpl(DataDao dao) {
        this.dao = dao;
    }

    private <T> T invoke(ServiceBody<T> daoBody) throws DataAccessException {
        try {
            return daoBody.invoke();
        }
        catch (NoDataException ex) {
            logger.info("No data received");
            throw new UnableAccessDataException(ex);
        }
        catch (NoConnectivityException ex) {
            logger.info("No connectivity");
            throw new UnableAccessDataException(ex);
        }
        catch (BackendException ex) {
            logger.info("Exception while getting data " + ex.getMessage());
            throw new DataAccessException(ex);
        }
    }

    private abstract class ServiceBody<T> {

        protected abstract T invoke() throws BackendException;
    }

    @Override
    public List<MobileScope> getMobileScopes() throws DataAccessException {
        return invoke(new ServiceBody<List<MobileScope>>() {

            @Override
            protected List<MobileScope> invoke() throws BackendException {
                return assembleMobileScopes(dao.getAvailableMobileScopes());
            }
        });
    }

    private List<MobileScope> assembleMobileScopes(List<ScopeInfoDto> dtos) {
        List<MobileScope> list = new ArrayList<MobileScope>();
        for (ScopeInfoDto dto : dtos) {
            MobileScope mobileScope = assembleMobileScope(dto);
            if (mobileScope.isVisible()) {
                list.add(mobileScope);
            }
        }
        return list;
    }

    private MobileScope assembleMobileScope(ScopeInfoDto dto) {
        MobileScope mobileScope = new MobileScope();
        mobileScope.setFolder(dto.getFolder());
        mobileScope.setName(dto.getName());
        mobileScope.setVisible(Boolean.parseBoolean(dto.getVisible()));
        mobileScope.setTransports(assembleTransports(dto.getTransports()));
        mobileScope.setStoredVersion(dto.getStoredVersion());
        return mobileScope;
    }

    @Override
    public MobileScope getScopeInfo(final String scopeName) throws DataAccessException {
        return invoke(new ServiceBody<MobileScope>() {

            @Override
            protected MobileScope invoke() throws BackendException {
                return assembleMobileScope(dao.getScopeInfo(scopeName));
            }
        });
    }

    private List<Transport> assembleTransports(List<TransportDto> dtos) {
        List<Transport> list = new ArrayList<Transport>();
        for (TransportDto dto : dtos) {
            list.add(assembleTransport(dto));
        }
        return list;
    }

    private Transport assembleTransport(TransportDto dto) {
        Transport transport = new Transport();
        transport.setId(dto.getId());
        transport.setType(TransportType.parseEnum(dto.getTransportType()));
        transport.setColor(dto.getColor());
        transport.setLocalizedName(dto.getLocalizedName());
        transport.setLocalizedNamePlural(dto.getLocalizedNamePlural());
        return transport;
    }

    @Override
    public List<String> getStopNames(final String scopeName) throws DataAccessException {
        return invoke(new ServiceBody<List<String>>() {

            @Override
            protected List<String> invoke() throws BackendException {
                return dao.getStopNames(scopeName);
            }
        });
    }

    @Override
    public List<Stop> getStops(final String scopeName, final boolean withSchedules, final String searchQuery, final Double lat,
            final Double lng, final Double distance) throws DataAccessException {
        return invoke(new ServiceBody<List<Stop>>() {

            @Override
            protected List<Stop> invoke() throws BackendException {
                return assembleStops(dao.getStops(scopeName, withSchedules, searchQuery, lat, lng, distance));
            }
        });
    }

    @Override
    public Stop getStopById(final String scopeName, final String stopId) throws DataAccessException {
        return invoke(new ServiceBody<Stop>() {

            @Override
            protected Stop invoke() throws BackendException {
                return assembleStop(dao.getStopById(scopeName, stopId));
            }
        });
    }

    @Override
    public List<ArrivalTime> getArrivalTimes(final String scopeName, final String stopId, final String stopFullName, final int maxMinutes,
            final int maxResults, final String time) throws DataAccessException {
        final Map<String, TransportType> transportIdToTypeMap = assembleTransportIdToTypeMap(getMobileScopes());
        return invoke(new ServiceBody<List<ArrivalTime>>() {

            @Override
            protected List<ArrivalTime> invoke() throws BackendException {
                return assembleArrivalTimes(dao.getArrivalTimes(scopeName, stopId, stopFullName, maxMinutes, maxResults, time),
                        transportIdToTypeMap);
            }
        });
    }

    @Override
    public List<Schedule> getSchedules(final String scopeName) throws DataAccessException {
        return invoke(new ServiceBody<List<Schedule>>() {

            @Override
            protected List<Schedule> invoke() throws BackendException {
                return assembleSchedules(dao.getSchedules(scopeName, null));
            }
        });
    }

    private List<Schedule> assembleSchedules(List<ScheduleDto> dtos) {
        List<Schedule> list = new ArrayList<Schedule>();
        for (ScheduleDto dto : dtos) {
            list.add(assembleSchedule(dto));
        }
        return list;
    }

    @Override
    public Schedule getScheduleById(final String scopeName, final String scheduleId) throws DataAccessException {
        return invoke(new ServiceBody<Schedule>() {

            @Override
            protected Schedule invoke() throws BackendException {
                return assembleSchedule(dao.getScheduleById(scopeName, scheduleId));
            }
        });
    }

    private Schedule assembleSchedule(ScheduleDto dto) {
        Schedule schedule = new Schedule();
        schedule.setId(dto.getId());
        schedule.setName(dto.getName());
        schedule.setTrackName(dto.getTrackName());
        schedule.setTransportType(TransportType.parseEnum(dto.getTransportType()));
        schedule.setTransportId(dto.getTransport());
        schedule.setValidComment(dto.getValidComment());
        return schedule;
    }

    @Override
    public List<Track> getTracks(final String scopeName, final String scheduleId) throws DataAccessException {
        return invoke(new ServiceBody<List<Track>>() {

            @Override
            protected List<Track> invoke() throws BackendException {
                return assembleTracks(dao.getTracks(scopeName, scheduleId));
            }
        });
    }

    private List<Track> assembleTracks(List<BusTrackDto> dtos) {
        List<Track> list = new ArrayList<Track>();
        for (BusTrackDto dto : dtos) {
            TrackType trackType = TrackType.parseEnum(dto.getTrackType());
            if (TrackType.NORMAL.equals(trackType) || TrackType.CHANGED.equals(trackType)) {
                list.add(assembleTrack(dto));
            }
        }
        return list;
    }

    @Override
    public Track getTrackById(final String scopeName, final String trackId) throws DataAccessException {
        return invoke(new ServiceBody<Track>() {

            @Override
            protected Track invoke() throws BackendException {
                return assembleTrack(dao.getTrackById(scopeName, trackId));
            }
        });
    }

    private Track assembleTrack(BusTrackDto dto) {
        Track track = new Track();
        track.setId(dto.getId());
        track.setName(dto.getName());
        track.setScheduleId(dto.getScheduleId());
        track.setTrackType(TrackType.parseEnum(dto.getTrackType()));
        return track;
    }

    @Override
    public List<Stop> getTrackStops(final String scopeName, final String trackId) throws DataAccessException {
        return invoke(new ServiceBody<List<Stop>>() {

            @Override
            protected List<Stop> invoke() throws BackendException {
                return assembleStops(dao.getTrackStops(scopeName, trackId));
            }
        });
    }

    private List<Stop> assembleStops(List<BusStopDto> dtos) {
        List<Stop> list = new ArrayList<Stop>();
        for (BusStopDto dto : dtos) {
            list.add(assembleStop(dto));
        }
        return list;
    }

    private Stop assembleStop(BusStopDto dto) {
        Stop stop = new Stop();
        stop.setId(dto.getId());
        stop.setName(dto.getName());
        stop.setFullName(dto.getFullName());
        stop.setDirectionName(dto.getDirectionName());
        stop.setLatitude(null == dto.getLat() ? null : new BigDecimal(dto.getLat()));
        stop.setLongitude(null == dto.getLng() ? null : new BigDecimal(dto.getLng()));
        stop.setSchedules(dto.getSchedules() != null ? assembleSchedules(dto.getSchedules()) : null);

        return stop;
    }

    @Override
    public List<DayGroup> getDifferentRunningDays(final String scopeName, final String scheduleId) throws DataAccessException {
        return invoke(new ServiceBody<List<DayGroup>>() {

            @Override
            protected List<DayGroup> invoke() throws BackendException {
                return assembleDaysOfWeek(dao.getDifferentRunningDays(scopeName, scheduleId));
            }
        });
    }

    private List<DayGroup> assembleDaysOfWeek(List<DaysOfWeekDto> dtos) {
        List<DayGroup> list = new ArrayList<DayGroup>();
        for (DaysOfWeekDto dto : dtos) {
            DayGroup daysOfWeek = new DayGroup(assembleWeekdays(dto.getDays()));
            list.add(daysOfWeek);
        }
        return list;
    }

    private Set<WeekdayType> assembleWeekdays(List<Integer> dtos) {
        Set<WeekdayType> set = EnumSet.noneOf(WeekdayType.class);
        for (Integer dto : dtos) {
            set.add(WeekdayType.parseEnumSunSat(dto));
        }
        return set;
    }

    @Override
    public StopArrivalTimes getStopTimes(final String scopeName, final String scheduleId, final String trackId, final String stopId) throws DataAccessException {

        return invoke(new ServiceBody<StopArrivalTimes>() {

            @Override
            protected StopArrivalTimes invoke() throws BackendException {
                List<StopDayTimesDto> stopDayTimes = dao.getStopDayTimes(scopeName, trackId, stopId);

                StopArrivalTimes stopArrivalTimes = new StopArrivalTimes();
                for (StopDayTimesDto arrivalTimesByDaysOfWeek : stopDayTimes) {
                    if (arrivalTimesByDaysOfWeek.getArrivalTimes() != null) {
                        DayGroup dayGroup = new DayGroup(arrivalTimesByDaysOfWeek.getDaysOfWeek().getDays());
                        stopArrivalTimes.addArrivalTimes(dayGroup, assembleArrivalTimes(arrivalTimesByDaysOfWeek.getArrivalTimes()));
                    }
                }

                return stopArrivalTimes;
            }
        });
    }

    private List<ArrivalTime> assembleArrivalTimes(List<ArrivalTimeDto> dtos) {
        return assembleArrivalTimes(dtos, null);
    }

    private List<ArrivalTime> assembleArrivalTimes(List<ArrivalTimeDto> dtos, Map<String, TransportType> transportIdToTypeMap) {
        List<ArrivalTime> list = new ArrayList<ArrivalTime>();
        for (ArrivalTimeDto dto : dtos) {
            ArrivalTime arrivalTime = new ArrivalTime();
            arrivalTime.setScheduledTime(new DateTime(dto.getScheduleTime()).getCalendar());
            arrivalTime.setExpectedTime(new DateTime(dto.getExpectedTime()).getCalendar());
            arrivalTime.setDirectionName(dto.getDirectionName());
            arrivalTime.setScheduleId(dto.getScheduleId());
            arrivalTime.setScheduleName(dto.getScheduleName());
            arrivalTime.setTrackId(dto.getTrackId());
            arrivalTime.setTrackType(TrackType.parseEnum(dto.getTrackType()));
            arrivalTime.setTransportId(dto.getTransport());
            arrivalTime.setStopDirectionName(dto.getStopDirectionName());
            arrivalTime.setRunId(dto.getRunId());
            arrivalTime.setStopName(dto.getStopName());
            arrivalTime.setStopId(dto.getStopId());
            arrivalTime.setIsAccessible(dto.getIsAccessible());
            if (null != transportIdToTypeMap) {
                arrivalTime.setTransportType(assembleTransportType(dto.getTransport(), transportIdToTypeMap));
            }
            list.add(arrivalTime);
        }
        return list;
    }

    @Override
    public RouteSearchResult findRoutes(final String scopeName, final String start, final String end, final String time, final boolean isArrival)
            throws DataAccessException {

        final Map<String, TransportType> transportIdToTypeMap = assembleTransportIdToTypeMap(getMobileScopes());

        return invoke(new ServiceBody<RouteSearchResult>() {

            @Override
            protected RouteSearchResult invoke() throws BackendException {
                return assembleRouteSearchResult(dao.findRoutes(scopeName, start, end, time, isArrival), transportIdToTypeMap);
            }
        });
    }

    private Map<String, TransportType> assembleTransportIdToTypeMap(List<MobileScope> mobileScopes) {
        Map<String, TransportType> map = new HashMap<String, TransportType>();
        for (MobileScope mobileScope : mobileScopes) {
            for (Transport transport : mobileScope.getTransports()) {
                map.put(transport.getId(), transport.getType());
            }
        }
        return map;
    }

    private RouteSearchResult assembleRouteSearchResult(SearchResultDto dto, Map<String, TransportType> transportIdToTypeMap) {
        RouteSearchResult result = new RouteSearchResult();
        result.setEndChoices(dto.getEndChoices());
        result.setStartChoices(dto.getStartChoices());
        result.setRoutes(assembleRoutes(dto.getRoutes(), transportIdToTypeMap));
        result.setEndStatus(parseRouteSearchStatus(dto.getEndStatus()));
        result.setStartStatus(parseRouteSearchStatus(dto.getStartStatus()));

        return result;
    }

    private RouteSearchStatus parseRouteSearchStatus(String startStatus) {
        if (StringUtils.equals(startStatus, "OK")) {
            return RouteSearchStatus.OK;
        }
        else if (StringUtils.equals(startStatus, "ClarifyName")) {
            return RouteSearchStatus.CLARIFY_NAME;
        }
        else if (StringUtils.equals(startStatus, "NameNotFound")) {
            return RouteSearchStatus.NAME_NOT_FOUND;
        }
        return null;
    }

    private List<Route> assembleRoutes(List<RouteDto> dtos, Map<String, TransportType> transportIdToTypeMap) {
        List<Route> list = new ArrayList<Route>();
        if (null != dtos) {
            for (RouteDto dto : dtos) {
                Route route = new Route();
                route.setSegments(assembleSegments(dto.getSegments(), transportIdToTypeMap));
                list.add(route);
            }
        }
        return list;
    }

    private List<Segment> assembleSegments(List<SegmentDto> dtos, Map<String, TransportType> transportIdToTypeMap) {
        List<Segment> list = new ArrayList<Segment>();
        for (SegmentDto dto : dtos) {
            Segment segment = new Segment();
            segment.setEndName(dto.getEndName());
            segment.setEndTime(new DateTime(dto.getEndTime()).getCalendar());

            segment.setScheduleId(dto.getScheduleId());
            segment.setScheduleName(dto.getScheduleName());

            segment.setRunId(dto.getRunId());
            segment.setTrackid(dto.getTrackid());

            segment.setStartName(dto.getStartName());
            segment.setStartTime(new DateTime(dto.getStartTime()).getCalendar());

            segment.setTransportType(assembleTransportType(dto.getTransport(), transportIdToTypeMap));
            segment.setTransportId(dto.getTransport());

            BusStopDto startStop = dto.getStartStop();
            segment.setStartStop(startStop != null ? assembleStop(startStop) : null);

            BusStopDto endStop = dto.getEndStop();
            segment.setEndStop(endStop != null ? assembleStop(endStop) : null);

            segment.setGeoPoints(assembleSegmentPoints(dto.getGeoPoints()));
            list.add(segment);
        }
        return list;
    }

    private List<SegmentPoint> assembleSegmentPoints(List<GeoPointDto> geoPoints) {
        List<SegmentPoint> list = new ArrayList<SegmentPoint>();

        if (geoPoints != null) {
            for (GeoPointDto geoPointDto : geoPoints) {
                SegmentPoint geoPoint = new SegmentPoint();
                geoPoint.setLatitude(geoPointDto.getLat() == null ? null : new BigDecimal(geoPointDto.getLat()));
                geoPoint.setLongitude(geoPointDto.getLng() == null ? null : new BigDecimal(geoPointDto.getLng()));
                geoPoint.setStopId(geoPointDto.getStopId());
                geoPoint.setStopName(geoPointDto.getStopName());
                geoPoint.setStopDirectionName(geoPointDto.getStopDirectionName());
                geoPoint.setTime(new DateTime(geoPointDto.getTime()).getCalendar());

                list.add(geoPoint);
            }
        }

        return list;
    }

    private TransportType assembleTransportType(String transportId, Map<String, TransportType> transportIdToTypeMap) {
        if (transportIdToTypeMap.containsKey(transportId)) {
            return transportIdToTypeMap.get(transportId);
        }
        else {
            return TransportType.WALK;
        }
    }

    @Override
    public List<Track> getTracksWithIds(String scopeName, String[] trackIds) throws DataAccessException {
        List<Track> list = new ArrayList<Track>();
        for (String trackId : trackIds) {
            list.add(getTrackById(scopeName, trackId));
        }
        return list;
    }

    @Override
    public List<ArrivalTime> getRunStops(final String scopeName, final String runId) throws DataAccessException {
        return invoke(new ServiceBody<List<ArrivalTime>>() {
            @Override
            protected List<ArrivalTime> invoke() throws BackendException {
                return assembleArrivalTimes(dao.getRunById(scopeName, runId).getTimes());
            }
        });
    }

    @Override
    public List<Transport> getTransports(final String scopeName) throws DataAccessException {
        return invoke(new ServiceBody<List<Transport>>() {
            @Override
            protected List<Transport> invoke() throws BackendException {
                return assembleTransports(dao.getTransports(scopeName));
            }
        });
    }
}
